package org.chipsalliance.diplomacy.nodes

import chisel3.{Data, DontCare, Wire}
import chisel3.experimental.SourceInfo
import org.chipsalliance.cde.config.{Field, Parameters}

import org.chipsalliance.diplomacy.ValName
import org.chipsalliance.diplomacy.sourceLine

/** One side metadata of a [[Dangle]].
  *
  * Describes one side of an edge going into or out of a [[BaseNode]].
  *
  * @param serial
  *   the global [[BaseNode.serial]] number of the [[BaseNode]] that this [[HalfEdge]] connects to.
  * @param index
  *   the `index` in the [[BaseNode]]'s input or output port list that this [[HalfEdge]] belongs to.
  */
case class HalfEdge(serial: Int, index: Int) extends Ordered[HalfEdge] {

  import scala.math.Ordered.orderingToOrdered

  def compare(that: HalfEdge): Int = HalfEdge.unapply(this).compare(HalfEdge.unapply(that))
}

/** [[Dangle]] captures the `IO` information of a [[LazyModule]] and which two [[BaseNode]]s the [[Edges]]/[[Bundle]]
  * connects.
  *
  * [[Dangle]]s are generated by [[BaseNode.instantiate]] using [[MixedNode.danglesOut]] and [[MixedNode.danglesIn]] ,
  * [[LazyModuleImp.instantiate]] connects those that go to internal or explicit IO connections in a [[LazyModule]].
  *
  * @param source
  *   the source [[HalfEdge]] of this [[Dangle]], which captures the source [[BaseNode]] and the port `index` within
  *   that [[BaseNode]].
  * @param sink
  *   sink [[HalfEdge]] of this [[Dangle]], which captures the sink [[BaseNode]] and the port `index` within that
  *   [[BaseNode]].
  * @param flipped
  *   flip or not in [[AutoBundle.makeElements]]. If true this corresponds to `danglesOut`, if false it corresponds to
  *   `danglesIn`.
  * @param dataOpt
  *   actual [[Data]] for the hardware connection. Can be empty if this belongs to a cloned module
  */
case class Dangle(source: HalfEdge, sink: HalfEdge, flipped: Boolean, name: String, dataOpt: Option[Data]) {
  def data = dataOpt.get
}

/** [[Edges]] is a collection of parameters describing the functionality and connection for an interface, which is often
  * derived from the interconnection protocol and can inform the parameterization of the hardware bundles that actually
  * implement the protocol.
  */
case class Edges[EI, EO](in: Seq[EI], out: Seq[EO])

/** A field available in [[Parameters]] used to determine whether [[InwardNodeImp.monitor]] will be called. */
case object MonitorsEnabled extends Field[Boolean](true)

/** When rendering the edge in a graphical format, flip the order in which the edges' source and sink are presented.
  *
  * For example, when rendering graphML, yEd by default tries to put the source node vertically above the sink node, but
  * [[RenderFlipped]] inverts this relationship. When a particular [[LazyModule]] contains both source nodes and sink
  * nodes, flipping the rendering of one node's edge will usual produce a more concise visual layout for the
  * [[LazyModule]].
  */
case object RenderFlipped extends Field[Boolean](false)

/** The sealed node class in the package, all node are derived from it.
  *
  * @param inner
  *   Sink interface implementation.
  * @param outer
  *   Source interface implementation.
  * @param valName
  *   val name of this node.
  * @tparam DI
  *   Downward-flowing parameters received on the inner side of the node. It is usually a brunch of parameters
  *   describing the protocol parameters from a source. For an [[InwardNode]], it is determined by the connected
  *   [[OutwardNode]]. Since it can be connected to multiple sources, this parameter is always a Seq of source port
  *   parameters.
  * @tparam UI
  *   Upward-flowing parameters generated by the inner side of the node. It is usually a brunch of parameters describing
  *   the protocol parameters of a sink. For an [[InwardNode]], it is determined itself.
  * @tparam EI
  *   Edge Parameters describing a connection on the inner side of the node. It is usually a brunch of transfers
  *   specified for a sink according to protocol.
  * @tparam BI
  *   Bundle type used when connecting to the inner side of the node. It is a hardware interface of this sink interface.
  *   It should extends from [[chisel3.Data]], which represents the real hardware.
  * @tparam DO
  *   Downward-flowing parameters generated on the outer side of the node. It is usually a brunch of parameters
  *   describing the protocol parameters of a source. For an [[OutwardNode]], it is determined itself.
  * @tparam UO
  *   Upward-flowing parameters received by the outer side of the node. It is usually a brunch of parameters describing
  *   the protocol parameters from a sink. For an [[OutwardNode]], it is determined by the connected [[InwardNode]].
  *   Since it can be connected to multiple sinks, this parameter is always a Seq of sink port parameters.
  * @tparam EO
  *   Edge Parameters describing a connection on the outer side of the node. It is usually a brunch of transfers
  *   specified for a source according to protocol.
  * @tparam BO
  *   Bundle type used when connecting to the outer side of the node. It is a hardware interface of this source
  *   interface. It should extends from [[chisel3.Data]], which represents the real hardware.
  *
  * @note
  *   Call Graph of [[MixedNode]]
  *   - line `─`: source is process by a function and generate pass to others
  *   - Arrow `→`: target of arrow is generated by source
  *
  * {{{
  *                                                                                                             (from the other node)
  *                                                  ┌─────────────────────────────────────────────────────────[[InwardNode.uiParams]]─────────────┐
  *                                                  ↓                                                                                             │
  *   (binding node when elaboration)   [[OutwardNode.uoParams]]────────────────────────[[MixedNode.mapParamsU]]→──────────┐                       │
  *      [[InwardNode.accPI]]                                                                      │                       │                       │
  *                  │                                                                             │            (based on protocol)                │
  *                  │                                                                             │            [[MixedNode.inner.edgeI]]          │
  *                  │                                                                             │                       ↓                       │
  *                  ↓                                                                             │                       │                       │
  * (immobilize after elaboration)   (inward port from [[OutwardNode]])                            │                       ↓                       │
  *      [[InwardNode.iBindings]]──┐     [[MixedNode.iDirectPorts]]────────────────────→[[MixedNode.iPorts]]   [[InwardNode.uiParams]]             │
  *                  │             │                ↑                                              │                       │                       │
  *                  │             │                │                                 [[OutwardNode.doParams]]             │                       │
  *                  │             │                │                                   (from the other node)              │                       │
  *                  │             │                │                                              │                       │                       │
  *                  │             │                │                                              │                       │                       │
  *                  │             │                │                                              └────────┬──────────────┤                       │
  *                  │             │                │                                                       │              │                       │
  *                  │             │                │                                                       │   (based on protocol)                │
  *                  │             │                │                                                       │   [[MixedNode.inner.edgeI]]          │
  *                  │             │                │                                                       │              │                       │
  *                  │             │     (from the other node)                                              │              ↓                       │
  *                  │             └───[[OutwardNode.oPortMapping]]  [[OutwardNode.oStar]]                  │   [[MixedNode.edgesIn]]───┐          │
  *                  │                              ↑                             ↑                         │              │            ↓          │
  *                  │                              │                             │                         │              │ [[MixedNode.in]]      │
  *                  │                              │                             │                         │              ↓            ↑          │
  *                  │   (solve star connection)    │                             │                         │   [[MixedNode.bundleIn]]──┘          │
  *                  ├───[[MixedNode.resolveStar]]→─┼─────────────────────────────┤                         └────────────────────────────────────┐ │
  *                  │                              │                             │                             [[MixedNode.bundleOut]]─┐        │ │
  *                  │                              │                             │                                        ↑            ↓        │ │
  *                  │                              │                             │                                        │ [[MixedNode.out]]   │ │
  *                  │                              ↓                             ↓                                        │            ↑        │ │
  *                  │            ┌─────[[InwardNode.iPortMapping]]    [[InwardNode.iStar]]                     [[MixedNode.edgesOut]]──┘        │ │
  *                  │            │       (from the other node)                                                            ↑                     │ │
  *                  │            │                 │                                                                      │                     │ │
  *                  │            │                 │                                                           [[MixedNode.outer.edgeO]]        │ │
  *                  │            │                 │                                                           (based on protocol)              │ │
  *                  │            │                 │                                                                      │                     │ │
  *                  │            │                 │                             ┌────────────────────────────────────────┤                     │ │
  *                  │            │                 │                             │                                        │                     │ │
  *                  │            │                 │                             │                                        │                     │ │
  *                  │            │                 │                             │                                        │                     │ │
  * (immobilize after elaboration)│                 ↓                             │                                        │                     │ │
  *     [[OutwardNode.oBindings]]─┘      [[MixedNode.oDirectPorts]]───→[[MixedNode.oPorts]]                   [[OutwardNode.doParams]]           │ │
  *                  ↑               (inward port from [[OutwardNode]])           │                                        │                     │ │
  *                  │                  ┌─────────────────────────────────────────┤                                        │                     │ │
  *                  │                  │                                         │                                        │                     │ │
  *                  │                  │                                         │                                        │                     │ │
  *     [[OutwardNode.accPO]]           │                                         ↓                                        │                     │ │
  *   (binding node when elaboration)   │ [[InwardNode.diParams]]─────→[[MixedNode.mapParamsD]]────────────────────────────┘                     │ │
  *                                     │             ↑                                                                                          │ │
  *                                     │             └──────────────────────────────────────────────────────────────────────────────────────────┘ │
  *                                     └──────────────────────────────────────────────────────────────────────────────────────────────────────────┘
  * }}}
  */
abstract class MixedNode[DI, UI, EI, BI <: Data, DO, UO, EO, BO <: Data](
  val inner:        InwardNodeImp[DI, UI, EI, BI],
  val outer:        OutwardNodeImp[DO, UO, EO, BO]
)(
  implicit valName: ValName)
    extends BaseNode
    with NodeHandle[DI, UI, EI, BI, DO, UO, EO, BO]
    with InwardNode[DI, UI, BI]
    with OutwardNode[DO, UO, BO] {
  // Generate a [[NodeHandle]] with inward and outward node are both this node.
  val inward  = this
  val outward = this

  /** Debug info of nodes binding. */
  def bindingInfo: String = s"""$iBindingInfo
                               |$oBindingInfo
                               |""".stripMargin

  /** Debug info of ports connecting. */
  def connectedPortsInfo: String = s"""${oPorts.size} outward ports connected: [${oPorts.map(_._2.name).mkString(",")}]
                                      |${iPorts.size} inward ports connected: [${iPorts.map(_._2.name).mkString(",")}]
                                      |""".stripMargin

  /** Debug info of parameters propagations. */
  def parametersInfo: String = s"""${doParams.size} downstream outward parameters: [${doParams.mkString(",")}]
                                  |${uoParams.size} upstream outward parameters: [${uoParams.mkString(",")}]
                                  |${diParams.size} downstream inward parameters: [${diParams.mkString(",")}]
                                  |${uiParams.size} upstream inward parameters: [${uiParams.mkString(",")}]
                                  |""".stripMargin

  /** For a given node, converts [[OutwardNode.accPO]] and [[InwardNode.accPI]] to [[MixedNode.oPortMapping]] and
    * [[MixedNode.iPortMapping]].
    *
    * Given counts of known inward and outward binding and inward and outward star bindings, return the resolved inward
    * stars and outward stars.
    *
    * This method will also validate the arguments and throw a runtime error if the values are unsuitable for this type
    * of node.
    *
    * @param iKnown
    *   Number of known-size ([[BIND_ONCE]]) input bindings.
    * @param oKnown
    *   Number of known-size ([[BIND_ONCE]]) output bindings.
    * @param iStar
    *   Number of unknown size ([[BIND_STAR]]) input bindings.
    * @param oStar
    *   Number of unknown size ([[BIND_STAR]]) output bindings.
    * @return
    *   A Tuple of the resolved number of input and output connections.
    */
  protected[diplomacy] def resolveStar(iKnown: Int, oKnown: Int, iStar: Int, oStar: Int): (Int, Int)

  /** Function to generate downward-flowing outward params from the downward-flowing input params and the current output
    * ports.
    *
    * @param n
    *   The size of the output sequence to generate.
    * @param p
    *   Sequence of downward-flowing input parameters of this node.
    * @return
    *   A `n`-sized sequence of downward-flowing output edge parameters.
    */
  protected[diplomacy] def mapParamsD(n: Int, p: Seq[DI]): Seq[DO]

  /** Function to generate upward-flowing input parameters from the upward-flowing output parameters [[uiParams]].
    *
    * @param n
    *   Size of the output sequence.
    * @param p
    *   Upward-flowing output edge parameters.
    * @return
    *   A n-sized sequence of upward-flowing input edge parameters.
    */
  protected[diplomacy] def mapParamsU(n: Int, p: Seq[UO]): Seq[UI]

  /** @return
    *   The sink cardinality of the node, the number of outputs bound with [[BIND_QUERY]] summed with inputs bound with
    *   [[BIND_STAR]].
    */
  protected[diplomacy] lazy val sinkCard: Int = oBindings.count(_._3 == BIND_QUERY) + iBindings.count(_._3 == BIND_STAR)

  /** @return
    *   The source cardinality of this node, the number of inputs bound with [[BIND_QUERY]] summed with the number of
    *   output bindings bound with [[BIND_STAR]].
    */
  protected[diplomacy] lazy val sourceCard: Int =
    iBindings.count(_._3 == BIND_QUERY) + oBindings.count(_._3 == BIND_STAR)

  /** @return list of nodes involved in flex bindings with this node. */
  protected[diplomacy] lazy val flexes: Seq[BaseNode] =
    oBindings.filter(_._3 == BIND_FLEX).map(_._2) ++ iBindings.filter(_._3 == BIND_FLEX).map(_._2)

  /** Resolves the flex to be either source or sink and returns the offset where the [[BIND_STAR]] operators begin
    * greedily taking up the remaining connections.
    *
    * @return
    *   A value >= 0 if it is sink cardinality, a negative value for source cardinality. The magnitude of the return
    *   value is not relevant.
    */
  protected[diplomacy] lazy val flexOffset: Int = {

    /** Recursively performs a depth-first search of the [[flexes]], [[BaseNode]]s connected to this node with flex
      * operators. The algorithm bottoms out when we either get to a node we have already visited or when we get to a
      * connection that is not a flex and can set the direction for us. Otherwise, recurse by visiting the `flexes` of
      * each node in the current set and decide whether they should be added to the set or not.
      *
      * @return
      *   the mapping of [[BaseNode]] indexed by their serial numbers.
      */
    def DFS(v: BaseNode, visited: Map[Int, BaseNode]): Map[Int, BaseNode] = {
      if (visited.contains(v.serial) || !v.flexibleArityDirection) {
        visited
      } else {
        v.flexes.foldLeft(visited + (v.serial -> v))((sum, n) => DFS(n, sum))
      }
    }

    /** Determine which [[BaseNode]] are involved in resolving the flex connections to/from this node.
      *
      * @example
      *   {{{
      *        a :*=* b :*=* c
      *        d :*=* b
      *        e :*=* f
      *   }}}
      *
      * `flexSet` for `a`, `b`, `c`, or `d` will be `Set(a, b, c, d)` `flexSet` for `e` or `f` will be `Set(e,f)`
      */
    val flexSet = DFS(this, Map()).values

    /** The total number of :*= operators where we're on the left. */
    val allSink = flexSet.map(_.sinkCard).sum

    /** The total number of :=* operators used when we're on the right. */
    val allSource = flexSet.map(_.sourceCard).sum

    require(
      allSink == 0 || allSource == 0,
      s"The nodes ${flexSet.map(_.name)} which are inter-connected by :*=* have ${allSink} :*= operators and ${allSource} :=* operators connected to them, making it impossible to determine cardinality inference direction."
    )
    allSink - allSource
  }

  /** @return A value >= 0 if it is sink cardinality, a negative value for source cardinality. */
  protected[diplomacy] def edgeArityDirection(n: BaseNode): Int = {
    if (flexibleArityDirection) flexOffset
    else if (n.flexibleArityDirection) n.flexOffset
    else 0
  }

  /** For a node which is connected between two nodes, select the one that will influence the direction of the flex
    * resolution.
    */
  protected[diplomacy] def edgeAritySelect(n: BaseNode, l: => Int, r: => Int): Int = {
    val dir = edgeArityDirection(n)
    if (dir < 0) l
    else if (dir > 0) r
    else 1
  }

  /** Ensure that the same node is not visited twice in resolving `:*=`, etc operators. */
  private var starCycleGuard = false

  /** Resolve all the star operators into concrete indicies. As connections are being made, some may be "star"
    * connections which need to be resolved. In some way to determine how many actual edges they correspond to. We also
    * need to build up the ranges of edges which correspond to each binding operator, so that We can apply the correct
    * edge parameters and later build up correct bundle connections.
    *
    * [[oPortMapping]]: `Seq[(Int, Int)]` where each item is the range of edges corresponding to that oPort (binding
    * operator). [[iPortMapping]]: `Seq[(Int, Int)]` where each item is the range of edges corresponding to that iPort
    * (binding operator). [[oStar]]: `Int` the value to return for this node `N` for any `N :*= foo` or `N :*=* foo :*=
    * bar` [[iStar]]: `Int` the value to return for this node `N` for any `foo :=* N` or `bar :=* foo :*=* N`
    */
  protected[diplomacy] lazy val (
    oPortMapping: Seq[(Int, Int)],
    iPortMapping: Seq[(Int, Int)],
    oStar:        Int,
    iStar:        Int
  ) = {
    try {
      if (starCycleGuard) throw StarCycleException()
      starCycleGuard = true
      // For a given node N...
      //   Number of foo :=* N
      // + Number of bar :=* foo :*=* N
      val oStars         = oBindings.count { case (_, n, b, _, _) =>
        b == BIND_STAR || (b == BIND_FLEX && edgeArityDirection(n) < 0)
      }
      //   Number of N :*= foo
      // + Number of N :*=* foo :*= bar
      val iStars         = iBindings.count { case (_, n, b, _, _) =>
        b == BIND_STAR || (b == BIND_FLEX && edgeArityDirection(n) > 0)
      }
      //   1         for foo := N
      // + bar.iStar for bar :*= foo :*=* N
      // + foo.iStar for foo :*= N
      // + 0         for foo :=* N
      val oKnown         = oBindings.map { case (_, n, b, _, _) =>
        b match {
          case BIND_ONCE  => 1
          case BIND_FLEX  => edgeAritySelect(n, 0, n.iStar)
          case BIND_QUERY => n.iStar
          case BIND_STAR  => 0
        }
      }.sum
      //   1         for N := foo
      // + bar.oStar for N :*=* foo :=* bar
      // + foo.oStar for N :=* foo
      // + 0         for N :*= foo
      val iKnown         = iBindings.map { case (_, n, b, _, _) =>
        b match {
          case BIND_ONCE  => 1
          case BIND_FLEX  => edgeAritySelect(n, n.oStar, 0)
          case BIND_QUERY => n.oStar
          case BIND_STAR  => 0
        }
      }.sum
      // Resolve star depends on the node subclass to implement the algorithm for this.
      val (iStar, oStar) = resolveStar(iKnown, oKnown, iStars, oStars)
      // Cumulative list of resolved outward binding range starting points
      val oSum           = oBindings.map { case (_, n, b, _, _) =>
        b match {
          case BIND_ONCE  => 1
          case BIND_FLEX  => edgeAritySelect(n, oStar, n.iStar)
          case BIND_QUERY => n.iStar
          case BIND_STAR  => oStar
        }
      }.scanLeft(0)(_ + _)
      // Cumulative list of resolved inward binding range starting points
      val iSum           = iBindings.map { case (_, n, b, _, _) =>
        b match {
          case BIND_ONCE  => 1
          case BIND_FLEX  => edgeAritySelect(n, n.oStar, iStar)
          case BIND_QUERY => n.oStar
          case BIND_STAR  => iStar
        }
      }.scanLeft(0)(_ + _)
      // Create ranges for each binding based on the running sums and return
      // those along with resolved values for the star operations.
      (oSum.init.zip(oSum.tail), iSum.init.zip(iSum.tail), oStar, iStar)
    } catch {
      case c: StarCycleException => throw c.copy(loop = context +: c.loop)
    }
  }

  /** Sequence of inward ports.
    *
    * This should be called after all star bindings are resolved.
    *
    * Each element is: `j` Port index of this binding in the Node's [[oPortMapping]] on the other side of the binding.
    * `n` Instance of inward node. `p` View of [[Parameters]] where this connection was made. `s` Source info where this
    * connection was made in the source code.
    */
  protected[diplomacy] lazy val oDirectPorts: Seq[(Int, InwardNode[DO, UO, BO], Parameters, SourceInfo)] =
    oBindings.flatMap { case (i, n, _, p, s) =>
      // for each binding operator in this node, look at what it connects to
      val (start, end) = n.iPortMapping(i)
      (start until end).map { j => (j, n, p, s) }
    }

  /** Sequence of outward ports.
    *
    * This should be called after all star bindings are resolved.
    *
    * `j` Port index of this binding in the Node's [[oPortMapping]] on the other side of the binding. `n` Instance of
    * outward node. `p` View of [[Parameters]] where this connection was made. `s` [[SourceInfo]] where this connection
    * was made in the source code.
    */
  protected[diplomacy] lazy val iDirectPorts: Seq[(Int, OutwardNode[DI, UI, BI], Parameters, SourceInfo)] =
    iBindings.flatMap { case (i, n, _, p, s) =>
      // query this port index range of this node in the other side of node.
      val (start, end) = n.oPortMapping(i)
      (start until end).map { j => (j, n, p, s) }
    }

  // Ephemeral nodes ( which have non-None iForward/oForward) have in_degree = out_degree
  // Thus, there must exist an Eulerian path and the below algorithms terminate
  @scala.annotation.tailrec
  private def oTrace(
    tuple: (Int, InwardNode[DO, UO, BO], Parameters, SourceInfo)
  ): (Int, InwardNode[DO, UO, BO], Parameters, SourceInfo) = tuple match {
    case (i, n, p, s) => n.iForward(i) match {
        case None         => (i, n, p, s)
        case Some((j, m)) => oTrace((j, m, p, s))
      }
  }

  @scala.annotation.tailrec
  private def iTrace(
    tuple: (Int, OutwardNode[DI, UI, BI], Parameters, SourceInfo)
  ): (Int, OutwardNode[DI, UI, BI], Parameters, SourceInfo) = tuple match {
    case (i, n, p, s) => n.oForward(i) match {
        case None         => (i, n, p, s)
        case Some((j, m)) => iTrace((j, m, p, s))
      }
  }

  /** Final output ports after all stars and port forwarding (e.g. [[EphemeralNode]]s) have been resolved.
    *
    * Each Port is a tuple of:
    *   - Numeric index of this binding in the [[InwardNode]] on the other end.
    *   - [[InwardNode]] on the other end of this binding.
    *   - A view of [[Parameters]] where the binding occurred.
    *   - [[SourceInfo]] for source-level error reporting.
    */
  lazy val oPorts: Seq[(Int, InwardNode[DO, UO, BO], Parameters, SourceInfo)] = oDirectPorts.map(oTrace)

  /** Final input ports after all stars and port forwarding (e.g. [[EphemeralNode]]s) have been resolved.
    *
    * Each Port is a tuple of:
    *   - numeric index of this binding in [[OutwardNode]] on the other end.
    *   - [[OutwardNode]] on the other end of this binding.
    *   - a view of [[Parameters]] where the binding occurred.
    *   - [[SourceInfo]] for source-level error reporting.
    */
  lazy val iPorts: Seq[(Int, OutwardNode[DI, UI, BI], Parameters, SourceInfo)] = iDirectPorts.map(iTrace)

  private var oParamsCycleGuard = false
  protected[diplomacy] lazy val diParams: Seq[DI] = iPorts.map { case (i, n, _, _) => n.doParams(i) }
  protected[diplomacy] lazy val doParams: Seq[DO] = {
    try {
      if (oParamsCycleGuard) throw DownwardCycleException()
      oParamsCycleGuard = true
      val o = mapParamsD(oPorts.size, diParams)
      require(
        o.size == oPorts.size,
        s"""Diplomacy has detected a problem with your graph:
           |At the following node, the number of outward ports should equal the number of produced outward parameters.
           |$context
           |$connectedPortsInfo
           |Downstreamed inward parameters: [${diParams.mkString(",")}]
           |Produced outward parameters: [${o.mkString(",")}]
           |""".stripMargin
      )
      o.map(outer.mixO(_, this))
    } catch {
      case c: DownwardCycleException => throw c.copy(loop = context +: c.loop)
    }
  }

  private var iParamsCycleGuard = false
  protected[diplomacy] lazy val uoParams: Seq[UO] = oPorts.map { case (o, n, _, _) => n.uiParams(o) }
  protected[diplomacy] lazy val uiParams: Seq[UI] = {
    try {
      if (iParamsCycleGuard) throw UpwardCycleException()
      iParamsCycleGuard = true
      val i = mapParamsU(iPorts.size, uoParams)
      require(
        i.size == iPorts.size,
        s"""Diplomacy has detected a problem with your graph:
           |At the following node, the number of inward ports should equal the number of produced inward parameters.
           |$context
           |$connectedPortsInfo
           |Upstreamed outward parameters: [${uoParams.mkString(",")}]
           |Produced inward parameters: [${i.mkString(",")}]
           |""".stripMargin
      )
      i.map(inner.mixI(_, this))
    } catch {
      case c: UpwardCycleException => throw c.copy(loop = context +: c.loop)
    }
  }

  /** Outward edge parameters. */
  protected[diplomacy] lazy val edgesOut: Seq[EO] =
    (oPorts.zip(doParams)).map { case ((i, n, p, s), o) => outer.edgeO(o, n.uiParams(i), p, s) }

  /** Inward edge parameters. */
  protected[diplomacy] lazy val edgesIn: Seq[EI] =
    (iPorts.zip(uiParams)).map { case ((o, n, p, s), i) => inner.edgeI(n.doParams(o), i, p, s) }

  /** A tuple of the input edge parameters and output edge parameters for the edges bound to this node.
    *
    * If you need to access to the edges of a foreign Node, use this method (in/out create bundles).
    */
  lazy val edges: Edges[EI, EO] = Edges(edgesIn, edgesOut)

  /** Create actual Wires corresponding to the Bundles parameterized by the outward edges of this node. */
  protected[diplomacy] lazy val bundleOut: Seq[BO] = edgesOut.map { e =>
    val x = Wire(outer.bundleO(e)).suggestName(s"${valName.value}Out")
    // TODO: Don't care unconnected forwarded diplomatic signals for compatibility issue,
    //       In the future, we should add an option to decide whether allowing unconnected in the LazyModule
    x := DontCare
    x
  }

  /** Create actual Wires corresponding to the Bundles parameterized by the inward edges of this node. */
  protected[diplomacy] lazy val bundleIn: Seq[BI] = edgesIn.map { e =>
    val x = Wire(inner.bundleI(e)).suggestName(s"${valName.value}In")
    // TODO: Don't care unconnected forwarded diplomatic signals for compatibility issue,
    //       In the future, we should add an option to decide whether allowing unconnected in the LazyModule
    x := DontCare
    x
  }

  private def emptyDanglesOut: Seq[Dangle] = oPorts.zipWithIndex.map { case ((j, n, _, _), i) =>
    Dangle(
      source = HalfEdge(serial, i),
      sink = HalfEdge(n.serial, j),
      flipped = false,
      name = wirePrefix + "out",
      dataOpt = None
    )
  }
  private def emptyDanglesIn:  Seq[Dangle] = iPorts.zipWithIndex.map { case ((j, n, _, _), i) =>
    Dangle(
      source = HalfEdge(n.serial, j),
      sink = HalfEdge(serial, i),
      flipped = true,
      name = wirePrefix + "in",
      dataOpt = None
    )
  }

  /** Create the [[Dangle]]s which describe the connections from this node output to other nodes inputs. */
  protected[diplomacy] def danglesOut: Seq[Dangle] = emptyDanglesOut.zipWithIndex.map { case (d, i) =>
    d.copy(dataOpt = Some(bundleOut(i)))
  }

  /** Create the [[Dangle]]s which describe the connections from this node input from other nodes outputs. */
  protected[diplomacy] def danglesIn: Seq[Dangle] = emptyDanglesIn.zipWithIndex.map { case (d, i) =>
    d.copy(dataOpt = Some(bundleIn(i)))
  }

  private[diplomacy] var instantiated = false

  /** Gather Bundle and edge parameters of outward ports.
    *
    * Accessors to the result of negotiation to be used within [[LazyModuleImp]] Code. Should only be used within
    * [[LazyModuleImp]] code or after its instantiation has completed.
    */
  def out: Seq[(BO, EO)] = {
    require(
      instantiated,
      s"$name.out should not be called until after instantiation of its parent LazyModule.module has begun"
    )
    bundleOut.zip(edgesOut)
  }

  /** Gather Bundle and edge parameters of inward ports.
    *
    * Accessors to the result of negotiation to be used within [[LazyModuleImp]] Code. Should only be used within
    * [[LazyModuleImp]] code or after its instantiation has completed.
    */
  def in: Seq[(BI, EI)] = {
    require(
      instantiated,
      s"$name.in should not be called until after instantiation of its parent LazyModule.module has begun"
    )
    bundleIn.zip(edgesIn)
  }

  /** Actually instantiate this node during [[LazyModuleImp]] evaluation. Mark that it's safe to use the Bundle wires,
    * instantiate monitors on all input ports if appropriate, and return all the dangles of this node.
    */
  protected[diplomacy] def instantiate(): Seq[Dangle] = {
    instantiated = true
    if (!circuitIdentity) {
      (iPorts.zip(in)).foreach { case ((_, _, p, _), (b, e)) => if (p(MonitorsEnabled)) inner.monitor(b, e) }
    }
    danglesOut ++ danglesIn
  }

  protected[diplomacy] def cloneDangles(): Seq[Dangle] = emptyDanglesOut ++ emptyDanglesIn

  /** Connects the outward part of a node with the inward part of this node. */
  protected[diplomacy] def bind(
    h:          OutwardNode[DI, UI, BI],
    binding:    NodeBinding
  )(
    implicit p: Parameters,
    sourceInfo: SourceInfo
  ): Unit = {
    val x = this // x := y
    val y = h
    sourceLine(sourceInfo, " at ", "")
    val i = x.iPushed
    val o = y.oPushed
    y.oPush(
      i,
      x,
      binding match {
        case BIND_ONCE  => BIND_ONCE
        case BIND_FLEX  => BIND_FLEX
        case BIND_STAR  => BIND_QUERY
        case BIND_QUERY => BIND_STAR
      }
    )
    x.iPush(o, y, binding)
  }

  /* Metadata for printing the node graph. */
  def inputs: Seq[(OutwardNode[DI, UI, BI], RenderedEdge)] = (iPorts.zip(edgesIn)).map { case ((_, n, p, _), e) =>
    val re = inner.render(e)
    (n, re.copy(flipped = re.flipped != p(RenderFlipped)))
  }

  /** Metadata for printing the node graph */
  def outputs: Seq[(InwardNode[DO, UO, BO], RenderedEdge)] = oPorts.map { case (i, n, _, _) => (n, n.inputs(i)._2) }
}
